package com.winsomely.util;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 主键生成器,生成的主键格式为:(最多10位时间戳(秒级) + 2位服务ID + 7位顺序递增数字) 类似于:1551670978 01 2156845
 * 
 * 性能: 
 * 1,在开发机测试 4*i5 6300HQ 2.30GHz 12G内存 64位Win10专业版
 * 2,四个线程200秒左右一共生成20亿个PK,平均每秒1000万个,完全满足需求
 * 
 * 该算法受到以下条件限制: 
 * 1,mysql数据库bigint类型对应java的long型,最大值为9223372036 85 4775807
 * 2,相应的,生成PK的时间戳部分的最大值减去 INIT_TIMES 后应为9223372036,对应的日期大概是2313-04-12
 * 3,因此在(2313-04-12)之后生产的PK将会超出数据库限制
 * 4,本算法没有对最大值做限制,因此在2313-04-12日期之后生成的PK需要使用者自己处理
 * 
 * @author liujiawei
 */
public class PKUtils {

	/**
	 * 主键结构递增部分的最小值
	 */
	private static final long ZERO = 0;

	/**
	 * 避免生成的时间戳太长,因此需要减去没用的历史时间(当前值为2021-01-01 00:00:00.000对应的时间戳,单位秒)
	 */
	private static final long INIT_TIMES = 1609430400L;

	/**
	 * 主键结构递增部分的最大值，保证同一毫秒内可以生成100万个递增变量
	 */
	private static final long MAX = 9999999;

	/**
	 * max的位数,和MAX有关,一般为MAX最大规划值加1
	 */
	private static final long MAX_DIGITS = 10000000L;

	/**
	 * nodeId的位数,和nodeId有关,一般为nodeId最大规划值加1
	 */
	private static final long NODE_ID_DIGITS = 100L;

	/**
	 * 主键结构递增部分的初始值
	 */
	private static long incremen = 0L;

	/**
	 * 记录上次的时间戳
	 */
	private static long lastTimes = -1L;

	/**
	 * 多节点部署时,每个节点的node.id,保证不同应用生成不重复的id
	 */
	private static long nodeId = 0L;

	/**
	 * 初始化nodeId
	 */
	static {
		nodeId = 88L;
		if (nodeId <= ZERO || nodeId >= NODE_ID_DIGITS)
			throw new IllegalArgumentException(
					MessageFormat.format("Error info: the nodeId must be {0} < nodeId < {1}", ZERO, NODE_ID_DIGITS));
	}

	/**
	 * 获取当前时间戳,单位秒
	 * 
	 * @return 当前时间戳,单位秒
	 */
	private static long curTimes() {
		return System.currentTimeMillis() / 1000;
	}

	/**
	 * 生成数据库主键，格式为(最多10位时间戳(秒级) + 2位服务ID + 7位顺序递增数字) 类似于：1551670978 01 2156845
	 * 
	 * @return 返回全局唯一的主键
	 */
	public synchronized static long nextPK() {
		long times = curTimes();
		if (times <= lastTimes) {
			incremen = incremen < MAX ? incremen + 1 : ZERO;
			if (incremen == ZERO) {
				while ((times = curTimes()) <= lastTimes);
			}
		} else {
			incremen = ZERO;
		}
		lastTimes = times;
		return ((times - INIT_TIMES) * NODE_ID_DIGITS + nodeId) * MAX_DIGITS + incremen;
	}

	/**
	 * 测试方法，可删除
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<>();
		map.put("k", 0);
		for (int i = 0; i < 4; i++) {			
			Thread thread = new Thread() {
				public void run() {
					int k = map.get("k") + 1;
					map.put("k", k);
					test("T" + k);
				}
			};
			thread.start();
			try {
				TimeUnit.MILLISECONDS.sleep(3456);
			} catch (InterruptedException e) {}
		}
	}

	/**
	 * 测试方法，可删除
	 * 
	 * @param name
	 */
	private static void test(String name) {
		long beginTime = System.currentTimeMillis();
		for (long i = 0; i < 5000000000L; i++) {
			nextPK();
		}
		long endTime = System.currentTimeMillis();
		System.out.println(name + " --------------------------: " + (endTime - beginTime));
	}

}
